ALB+Cognito環境でバックエンドに渡されるx-amzn-oidc-dataを検証する
はじめに
ALBとCognitoを連携して認証すると、下記の3つのユーザークレームがHTTPヘッダーに付与されて、ALBのターゲットに送信されます。
- x-amzn-oidc-accesstoken(トークンエンドポイントからのアクセストークン)
- x-amzn-oidc-identity(ユーザー情報エンドポイントからの
sub
フィールド) - x-amzn-oidc-data(JWT形式のユーザークレーム)
こちらについては以下の記事でも詳しく説明されていますので、合わせてご参照ください。
さて、3つ目の x-amzn-oidc-data
は、ユーザー情報を含むJWT形式のクレームです。このユーザー情報とは、OIDCの userInfoエンドポイント
から取得されたものになります。今回の場合はCognitoのエンドポイントから取得されたものです。
アプリケーション側では、このクレームから認証されたユーザーの情報を取得することができます。しかし、JWTのヘッダー部やペイロード部はBase64エンコードされているだけなので、容易に改ざんできてしまいます。つまり、これを安全に取り扱うためには、署名の検証を実施する必要があります。
前提
以下の環境で検証処理を実装してみます。
- Java: 21.0.5
- Nimbus JOSE JWT: 10.10.1
署名の検証
まずは x-amzn-oidc-data
のJWTヘッダーから kid
を取得します。
SignedJWT signedJWT = SignedJWT.parse(oidcData);
String kid = signedJWT.getHeader().getKeyID();
また、このJWTヘッダーには signer
というフィールドも含まれます。直訳すると「署名者」で、このトークンを署名した人になります。x-amzn-oidc-data
で渡されるクレームはALB自身によって署名されたものなので、ここの値は対象ALBのARNとなっているはずです。
署名の検証を始める前に、この値が想定した値と一致しているかチェックしましょう。
String expectedAlbArn = "arn:aws:elasticloadbalancing:ap-northeast-1:111111111111:loadbalancer/app/hoge/xxxxxxxxxxxxxxxx";
String signer = (String) signedJWT.getHeader().getCustomParam("signer");
if (!expectedAlbArn.equals(signer)) {
throw new RuntimeException("Signer validation failed");
}
公開鍵は https://public-keys.auth.elb.{region}.amazonaws.com/{kid}
というエンドポイントから取得できるので、先ほど取得した kid
を使ってリクエストします。PEM形式の公開鍵が降ってくるので、これを変数に格納しておきます。
URI url = new URI("https://public-keys.auth.elb." + awsRegion + ".amazonaws.com/" + kid);
HttpClient client = HttpClient.newBuilder().build();
HttpRequest request = HttpRequest.newBuilder().uri(url).GET().build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
String publicKeyPem = response.body();
JavaではPEM形式をそのまま使うことはできず、公開鍵オブジェクトにしてあげる必要があります。
まず、先頭と末尾の行(-----BEGIN PUBLIC KEY-----
など)や改行や空白行を取り除いてからBase64デコードし、DER形式(バイナリ)にします。
そのバイナリから X509EncodedKeySpec
を生成し、KeyFactory
でアルゴリズムを指定して公開鍵オブジェクトを生成するという手順になります。
ALBでは ES256(楕円曲線を使用した署名アルゴリズム)
を使用して署名していますので、KeyFactory
は楕円曲線(EC)を指定します。
byte[] publicDer = Base64.getDecoder().decode(publicKeyPem.replaceAll("-----.+?-----", "").replaceAll("\\r?\\n", "").trim());
X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicDer);
KeyFactory kf = KeyFactory.getInstance("EC");
ECPublicKey publicKey = (ECPublicKey) kf.generatePublic(publicKeySpec);
この公開鍵オブジェクトを用いて、 Nimbus JOSE + JWT
ライブラリを使って署名を検証します。
ECDSAVerifier verifier = new ECDSAVerifier(publicKey);
boolean result = signedJWT.verify(verifier);
ここまできて、JWTペイロードを信頼できるようになります。
有効期限の検証
リプレイアタックを防ぐためにも、有効期限チェックは必ず実施しましょう。JWTペイロードからクレームセットを取得して、タイムスタンプを比較すれば大丈夫です。
JWTClaimsSet claims = signedJWT.getJWTClaimsSet();
if (claims.getExpirationTime().before(new java.util.Date())) {
throw new RuntimeException("Token expired");
}
おわりに
ALBとCognitoを連携させると、とても簡単に認証アプリを構築することができます。バックエンドのアプリケーション側で認証されたユーザーを特定したいといった場合にも、ALBから渡される値を利用することで比較的簡単に実装することが可能です。
とは言え、その値をそのまま使うのは少し危険ですので、今回のように検証を実施することを推奨します。